﻿using SharpDepend;
using SoundEngine3.MIDI.Meta;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;

namespace ZquenceStudio3Host.DspPlugins.Synthesizers.Nodesizer.Engine
{
    public class WaveFormBuffer
    {
        public double[] Buffer { get; private set; }

        public int Length { get { return Buffer.Length; } }
        
        public WaveFormBuffer()
        {
            Setup(1);
        }

        public void Setup(int size)
        {
            if (Buffer == null  || size != Buffer.Length)
            {
                Buffer = new double[size == 0 ? 1 : size];
            }
        }

        public double this[int index]
        {
            get
            {
                return Buffer[index];
            }
            set
            {
                Buffer[index] = value;
            }
        }

        public double this[int sampleIndex, int index]
        {
            get
            {
                return Buffer[index];
            }
            set
            {
                Buffer[index] = value;
            }
        }

        public void Clear()
        {
            System.Array.Clear(Buffer, 0, Buffer.Length);
        }

        internal void SetBuffer(double[] buffer)
        {
            Buffer = buffer;
        }
    }

    public class ModuleOutput : ModuleIO
    {
        private List<ModuleInput> mConnectedInputs;
        private ReadOnlyCollection<ModuleInput> mConnectedInputsReadOnly;
        private bool mHasGotMidiMessage;
        private double mVisualForce;

        public void AddInput(ModuleInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException(nameof(input));
            }
            if (mConnectedInputs.Contains(input))
            {
                throw new Exception("Input already added to this output.");
            }

            // Check if types is matched.
            if (!MatchType(input.ActualType))
            {
                throw new NotSupportedException(string.Format("Input '{0}' of type {1} can not be connected to output '{2}' with type of {3}.", input.Name, input.ActualType, this.Name, this.ActualType));
            }

            // If types are not directly matched, then a type switch needs.
            if (!IsDirectMatchType(input.ActualType))
            {
                // Only works if no input or output are conntected.
                if(!PossibleToConnect(input))
                {
                    throw new Exception("Can not switch Type when there is other input or outputs connected with another Type.");
                }
            }

            if (Type == IOType.AudioOrWaveform && input.Type == IOType.AudioOrWaveform)
            {
                IOType prefer = ActualType == IOType.AudioOrWaveform ? input.ActualType : ActualType;

                // Re-setup all input and output with IOType of AudioOrWaveform.
                input.Module.ResetupSwitchableInputAndOutputs(prefer);
                this.Module.ResetupSwitchableInputAndOutputs(prefer);
            }
            else if (Type == IOType.AudioOrWaveform && input.Type != IOType.AudioOrWaveform)
            {
                // Re-setup all input and output with IOType of AudioOrWaveform.
                this.Module.ResetupSwitchableInputAndOutputs(input.ActualType);
            }
            else if (Type != IOType.AudioOrWaveform && input.Type == IOType.AudioOrWaveform)
            {
                // Re-setup all input and output with IOType of AudioOrWaveform.
                input.Module.ResetupSwitchableInputAndOutputs(ActualType);
            }
            
            mConnectedInputs.Add(input);

            RaiseOnConnected(this, input);

            input.FromOutput = this;
        }

        public void RemoveInput(ModuleInput input)
        {
            if (input == null)
            {
                throw new ArgumentNullException(nameof(input));
            }
            if (!mConnectedInputs.Contains(input))
            {
                throw new Exception("Input do not exist in this output.");
            }

            mConnectedInputs.Remove(input);

            RaiseOnDisconnected(this, input);

            input.FromOutput = null;

            // If module has switchable input/output, when if there is only connectin to switchable input/outputs set the type to AudioOrWaveform.
            if (!Module.IsConnectedToNonSwitchable)
            {
                Module.ResetupSwitchableInputAndOutputs(IOType.AudioOrWaveform);
            }
            if (!input.Module.IsConnectedToNonSwitchable)
            {
                input.Module.ResetupSwitchableInputAndOutputs(IOType.AudioOrWaveform);
            }
        }
        
        public ReadOnlyCollection<ModuleInput> ConnectedInputs
        {
            get { return mConnectedInputsReadOnly; }
        }

        public void DisconnectAllInputs()
        {
            for (int i = mConnectedInputs.Count - 1; i >= 0; i--)
            {
                mConnectedInputs[i].FromOutput = null;
            }
        }

        public override double VisualForce
        {
            get
            {
                return mVisualForce;
            }
        }

        /// <summary>
        /// If Audio, the buffer size will be as the same buffer size as the sound card.
        /// If WaveForm, the buffer size should be...
        /// </summary>
        public WaveFormBuffer AudioBuffer { get; private set; }

        /// <summary>
        /// Get midi buffer.
        /// </summary>
        public List<MidiMessage> MidiBuffer { get; private set; }
        
        public bool HasGotMidiMessage()
        {
            return mHasGotMidiMessage;
        }
        
        internal void Processed()
        {
            mHasGotMidiMessage = false;
            
            if (mVisualForce >= 0)
            {
                mVisualForce -= 0.1;

                if (mVisualForce < 0)
                {
                    mVisualForce = 0;
                }
            }

            if(ActualType != IOType.Midi)
            {
                const int NUMBER = 10;
                mVisualForce = 0;
                double iPre = (double)AudioBuffer.Buffer.Length / NUMBER;
                for (int i = 0; i < NUMBER; ++i)
                {
                    double value = Math.Abs(AudioBuffer.Buffer[(int)(i * iPre)]);

                    mVisualForce = Math.Max(mVisualForce, value);
                }
            }
            else
            {
                if (mNoteRunning > 0)
                {
                    if (mVisualForce < 0.2)
                    {
                        mVisualForce = 0.2;
                    }
                }
            }
        }

        int mNoteRunning = 0;

        /// <summary>
        /// Add collection of midi messages and process a shortcut for midi message buffer frame start.
        /// </summary>
        /// <param name="message"></param>
        public void AddMidiMessage(List<MidiMessage> messages)
        {
            foreach (var message in messages)
            {
                AddMidiMessage(message);
            }
        }

        /// <summary>
        /// Add a midi message and process a shortcut for midi message buffer frame start.
        /// </summary>
        /// <param name="message"></param>
        public void AddMidiMessage(MidiMessage message)
        {
            mHasGotMidiMessage = true;

            if (Type != IOType.Midi)
            {
                throw new InvalidOperationException("AddMidiMessage method can only be called if type is set to Midi in constructor.");
            }

            int index = (int)message.DeltaFrame;

            if (message.MidiData.Status == MidiStatus.NOTE_ON)
            {
                mVisualForce = 1;
                mNoteRunning++;
                SetBuffer(AudioBuffer.Buffer, index, NOTE_ON);

                // Every ok, add it.
                MidiBuffer.Add(message);
            }
            else if (message.MidiData.Status == MidiStatus.NOTE_OFF)
            {
                mNoteRunning--;

                if (mNoteRunning < 0)
                {
                    //throw new Exception("Unexpected midi state in ModuleOutput");

                    // NOTE: May be when a user press a note in UI and sudently change the synthesizer for the UI
                    //       and user unrelease the note, and then we got a note off.
                    Log.Assert(mNoteRunning >= 0, "Unexpected midi state in ModuleOutput");
                    mNoteRunning = 0;
                }
                else
                {
                    SetBuffer(AudioBuffer.Buffer, index, NOTE_OFF);

                    // Every ok, add it.
                    MidiBuffer.Add(message);
                }
            }
            else
            {
                SetBuffer(AudioBuffer.Buffer, index, INVALID);

                // Every ok, add it.
                MidiBuffer.Add(message);
            }
        }

        /// <summary>
        /// Try to set value to buffer at specific index, but if the index is busy, set it to the last
        /// and move other values to the left.
        /// </summary>
        private static void SetBuffer(double[] buffer, int index, double value)
        {
            reverseFix:;
            if (index == buffer.Length)
            {
                double temp = value;
                double temp2 = 0;
                for (int i = buffer.Length - 1; i >= 0; i--)
                {
                    if (buffer[i] != INVALID)
                    {
                        temp2 = buffer[i - 1];
                        buffer[i - 1] = buffer[i];

                        buffer[i] = temp;

                        temp = temp2;

                        if (buffer[i - 2] == INVALID)
                        {
                            // If buffer is unnormal smal we can get problems:
                            // [0] 0.9 <-- This is the first one and there is no space for free value. [i - 2] will throw exception.
                            // [1] 1
                            // [2] 0.9
                            // [3] 1

                            // Practice example:
                            // [508] 0
                            // [509] 0 <-- Now this is free value and we can break this.
                            // [510] 0.9
                            // [511] 1
                            break;
                        }
                    }
                    else
                    {
                        buffer[i] = temp;
                        break;
                    }
                }
            }
            else
            {
                while (buffer[index] != INVALID)
                {
                    index++;
                    if (index == buffer.Length)
                    {
                        goto reverseFix;
                    }
                }
                buffer[index] = value;
            }
        }

        /// <summary>
        /// Get or set if the buffer has been processed and is ready.
        /// </summary>
        public bool ReadyBuffer
        {
            get
            {
                return Module.Processed;
            }
        }

        public override bool IsCircularConnected
        {
            get
            {
                // Loop until we find myself.
                return FindModuleViaOutputs(Module, Module);
            }
        }
        
        public override bool IsConnected
        {
            get
            {
                return ConnectedInputs.Count > 0;
            }
        }

        public ModuleOutput(string name, IOType type, BaseModule module)
            : this(name, name, type, module)
        {
        }

        public ModuleOutput(string shortName, string name, IOType type, BaseModule module)
            : base(shortName, name, type, module)
        {
            mConnectedInputs = new List<ModuleInput>();
            mConnectedInputsReadOnly = new ReadOnlyCollection<ModuleInput>(mConnectedInputs);
            AudioBuffer = new WaveFormBuffer();
            MidiBuffer = new List<MidiMessage>();

            SetupAudioBuffer(this, AudioBuffer);
        }

        public bool CheckCircularConnection(ModuleInput ifConnectedWith)
        {
            bool isCircularConnected = false;

            ModuleIO.SetDisabledCallbacks(true);
            
            AddInput(ifConnectedWith);

            isCircularConnected = IsCircularConnected;

            RemoveInput(ifConnectedWith);

            ModuleIO.SetDisabledCallbacks(false);

            return isCircularConnected;
        }

        internal override void ResetupSwitchable(IOType type)
        {
            base.ResetupSwitchable(type);

            SetupAudioBuffer(this, AudioBuffer);
        }

        internal static void SetupAudioBuffer(ModuleIO moduleIO, WaveFormBuffer audioBuffer)
        {
            switch (moduleIO.ActualType)
            {
                case BaseModule.NO_TYPE:
                    // Do nothing.
                    break;
                case IOType.Audio:
                    {
                        audioBuffer.Setup(moduleIO.Module.SoundEngineContext.BufferSize);
                    }
                    break;
                case IOType.Midi:
                    {
                        audioBuffer.Setup(moduleIO.Module.SoundEngineContext.BufferSize);
                    }
                    break;
                case IOType.Waveform:
                    {
                        audioBuffer.Setup(WAVEFORM_BUFFER_LENGTH);
                    }
                    break;
                default:
                    //throw new InvalidOperationException("No possible IOType");
                    break;
            }
        }

        internal static int GetBufferLength(ModuleIO moduleIO)
        {
            switch (moduleIO.ActualType)
            {
                case IOType.Audio:
                case IOType.Midi:
                case IOType.AudioOrWaveform:
                    return moduleIO.Module.SoundEngineContext.BufferSize;
                case IOType.Waveform:
                    return WAVEFORM_BUFFER_LENGTH;
                default:
                    break;
            }

            throw new Exception("");
        }

        public const int WAVEFORM_BUFFER_LENGTH = 1000;

        public override string ToString()
        {
            return base.ToString() + " - Output: " + Name;
        }
        
        public void SetBufferSize(int bufferSize)
        {
            //AudioBuffer = new double[bufferSize];
            SetupAudioBuffer(this, AudioBuffer);
        }
    }
}
